Refresh your Java skills–聊聊Java9 中模块化设计是如何实现类似IOC依赖注入效果及与其区别
场景引入
如何实现IOC的效果,我们可以来想想,无非就是一个隐式实现,而想要做到,总不能什么都没有,来个巧妇难为无米之炊的境地吧,所以说,米必须要有滴,在Spring中就是一个bean,也就是说,容器里得有米,再官话点就是上下文中得存在所需要的bean。同样模块化中两个互相隔离的模块想要达到这种效果,也要先往jvm里扔个对象进去的,然后who use ,who get 就可以了。
请看例子(可以认为是我们平常写的SpringMVC项目中的service->serviceImpl->controller):
service接口化模块
1 | package com.example.api; |
上面这个接口所在的模块定义:
1 | module migo.codec.api { |
serviceImpl化模块
接着,我们定义一个实现模块:
1 | module migo.codec.service { |
具体实现就省略了。
controller化模块
最后我们在最上层的模块内使用:
1 | module migo.codec.controller { |
具体的controller模块内使用的代码如下:
1 | ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class); |
或者:
1 | public static void main(String... args) { |
亦或者假如有很多服务实现的提供者,而某个提供服务实现的provider(也就是serviceImpl)上面有添加注解@PNG
,而我们想使用带有这个注解的实例,可以使用以下代码:
1 | ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class); |
内部工作机制原理
具体思路:
通过在模块定义里面的provides aaa with aaaImpl 这个功能,可以很容易的想到key value
组合
当我们碰到这对关键字的时候,我们就会解析并将aaa
做为key
,aaaImpl
添加到一个list
中并将这个list
作为value
,并添加到一个Map<String,list>
中
在我们碰到uses
关键字(源码里面acc
会去确定这个权限),并通过ServiceLoader.load(key)
来找到这个key所对应的一个包含了实现类具体地址的list,可能有多个,那么,拓展功能,我们使用一个装饰模式,也就是继承了Iterable
这个接口,可以达到遍历并生成具体实例来达到要求。
源码解析
确定米粒的路径
那么按照这个思路,我们反着来找下,这里只列关键代码:
从上面的Demo中,我们可以看到,通过类的class字节码来加载:
之前有说,巧妇难为无米之炊,所以这个上下文很重要,我们的类加载器也是要讲究上下文的
1 | /** |
我们进去这个ServiceLoader
,其实无非就是一个构造器而已了,关键代码我截下:
1 | this.service = svc; |
有了这个加载器之后,其实我们就拿到了上下文和访问权限的一些东西,我们再来看看这个类的字段:
1 | public final class ServiceLoader<S> |
可以看到,它实现了按照我们分析的Iterable
接口,这样我们就可以多了很多操作,而且我们也看到了下面这几个东西,这样我们就可以做事情了:
1 | private Iterator<Provider<S>> lookupIterator2; |
我们走进findFirst
这个方法来看看:
1 | public Optional<S> findFirst() { |
我们看到了iterator()
这个方法:
1 | public Iterator<S> iterator() { |
现在newLookupIterator()
进入到我们的视野中,没有条件创建条件,刚开始我们可没有拿到米,现在去找米去:
1 | /** |
这里抛开其他我们来看ModuleServicesLookupIterator()
这个构造函数 :
1 | ModuleServicesLookupIterator() { |
映入眼帘的是iteratorFor(ClassLoader loader)
这个方法:
1 | /** |
这里终于找到了findServices(String service)
这个方法:
1 | /** |
结合getOrDefault
的源码可知:
1 | default V getOrDefault(Object key, V defaultValue) { |
是不是和我们的具体思路接上轨了
拿到我们想要的大米
而我们的provider
实例从何而来,请容我娓娓道来咯:
我们从jdk.internal.module.Modules
这个模块定义类中可以找到addProvides
这个方法,也就是说在我们加载这个模块的时候,这个动作就已经要干活了:
1 | /** |
然后我们可以从sun.instrument.InstrumentationImpl
这个类来看到其工作方式(通过其注释就可以看到这个类和JVM相关):
在加载模块的时候就执行了下面的代码,看下面update provides
这个注释的代码可以知道其调用了上面的addProvides
这个方法,而最后也是调用了addProvider(m, service, impl)
1 | /** |
Instrumentation
接口有一段很重要的注释,大家自己看吧,就不多说了:
1 | /** |
那么,我们最后,走入addProvider(m, service, impl)
这个方法中:
1 | /** |
再经过了这么曲曲折折的过程,终于拿到了ServiceProvider
,里面包括了我们所要调用实现类的地址信息
于是,看下ServiceLoader这个类定义的Provider
静态内部接口:
1 |
|
然后我们回到之前追到的iteratorFor
方法,知道其返回的是 Iterator<ServiceProvider>
类型
1 | /** |
然后回到ModuleServicesLookupIterator()
这个构造函数,直接看这个内部类,也就是调用这个
1 | /** |
在newLookupIterator
这个方法中得到ModuleServicesLookupIterator
的实例first
,并调用其hasNext
方法
1 | /** |
我们来进入这个hasNext
方法,也就是在这里,调用了loadProvider
生成了一个bean
1 |
|
走进这个loadProvider
方法,抛开前面所有,我们只看最后返回为:new ProviderImpl<S>(service, type, ctor, acc)
1 | /** |
最后,我们通过查看这个ProviderImpl
类终于得到了我们想要得到的结果。
1 | /** |
IOC和模块化所提供的类似效果的最大的区别就是,前者是提供了实例化的bean(即便是通过AOP实现的,这点很重要,Java9模块化在使用Spring的时候会有特别的设置),而且是基于Spring容器的单例的存在(多例注入的问题请参考我这方面的Spring源码解析),后者是提供了class字节码所在的路径,用的时候内部会自行生成实例,所以是多例的。
其实整个过程,Java的模块化文件系统起了很大的作用(这块看情况假如篇幅比较长久不放在我的书里了),然后自己追源码的思路也在这里给大家展现了一番,希望可以对大家有所帮助,看源码不要上来就瞎找的。另外,最重要的一点就是,不要因为源码很多,很复杂就轻言放弃,看的多了,看的久了,自然就有一套属于自己的方法论了。